package utilities.manager;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Properties;

import javax.swing.JPanel;

import utilities.control.iface.UserControlListener;

/**
 * 
 * @author CoconutTank
 *
 */
public abstract class Runner extends JPanel implements Runnable, UserControlListener
{
	private static final long serialVersionUID = -6966017380605239731L;
	
	//public static final Dimension DEFAULT_SIZE = new Dimension(400, 300);
	public static final int DEFAULT_WIDTH = 400;
	public static final int DEFAULT_HEIGHT = 300;

	protected Thread animator; // should be one animator per panel?
	
	// Is the game still running?
	protected volatile boolean isRunning = false;
	
	protected volatile boolean isPaused = false;

	// For double buffering draws
	protected Graphics2D doubleBufferedGraphics = null;
	protected BufferedImage doubleBufferedImage = null;
	
	// How fast should the game render?
	// Should user be able to determine this value?
	protected double framesPerSecond = 1 / 60.0;
	
	protected double globalScaling = Math.floor(1.0);
	
	// Mouse position information
	protected int mouseX;
	protected int mouseY;
	
	// The variables that are inherent to this particular runner and can be
	// distributed to other runners or modules without problems.
	private Properties globals;
	
	/*******************************************************************
	 * 
	 * CONSTRUCTORS
	 * 
	 *******************************************************************/
	
	/**
	 * 
	 * @param width
	 * @param height
	 * @param backgroundColor
	 */
	public Runner(int width, int height, Color backgroundColor)
	{
		this.setSize(new Dimension((width < 1 ? DEFAULT_WIDTH : width), (height < 1 ? DEFAULT_HEIGHT : height)));
		this.setBackground(backgroundColor == null ? Color.white : backgroundColor);
		this.globals = new Properties();
	}
	
	/*******************************************************************
	 * 
	 * INHERITED METHODS
	 * 
	 *******************************************************************/

	public void keyTyped(KeyEvent e) {}
	
	public void keyPressed(KeyEvent e) {}

	public void keyReleased(KeyEvent e) {}
	
	public void mouseClicked(MouseEvent e) { setMouseXY(e); }

	public void mousePressed(MouseEvent e) { setMouseXY(e); }

	public void mouseReleased(MouseEvent e) { setMouseXY(e); }

	public void mouseEntered(MouseEvent e) { setMouseXY(e); }

	public void mouseExited(MouseEvent e) { setMouseXY(e); }

	public void mouseDragged(MouseEvent e) { setMouseXY(e); }

	public void mouseMoved(MouseEvent e) { setMouseXY(e); }

	public void mouseWheelMoved(MouseWheelEvent e) { setMouseXY(e); }
	
	/*******************************************************************
	 * 
	 * ABSTRACT METHODS
	 * 
	 *******************************************************************/
	
	/**
	 * @param globals
	 * 
	 */
	public abstract void gameUpdate(Properties globals);
	
	/**
	 * 
	 * @param g
	 */
	public abstract void drawScreen(Graphics g);
	
	/*******************************************************************
	 * 
	 * METHODS
	 * 
	 *******************************************************************/
	
	/**
	 * 
	 */
	public final void start()
	{
		if(animator == null || !isRunning)
		{
			animator = new Thread(this);
			animator.start();
		}
		
		isRunning = true;
		isPaused = false;
	}
	
	/**
	 * 
	 */
	public final void toggleRunning()
	{
		isRunning = !isRunning;
	}
	
	/**
	 * 
	 * @return
	 */
	public final boolean isRunning()
	{
		return isRunning;
	}
	
	/**
	 * 
	 */
	public final void togglePaused()
	{
		isPaused = !isPaused;
	}
	
	/**
	 * 
	 * @return
	 */
	public final boolean isPaused()
	{
		return isPaused;
	}
	
	/**
	 * 
	 * @param key
	 * @param value
	 */
	public final String setVar(String key, String value)
	{
		return (globals != null ? (String)globals.setProperty(key, value) : null);
	}
	
	/**
	 * 
	 * @param key
	 * @return
	 */
	public final String getVar(String key)
	{
		return (globals != null ? globals.getProperty(key) : null);
	}

	/**
	 * 
	 */
	public final void run()
	{
		while(isRunning)
		{
			// Do not update the game if it is paused!
			if(!isPaused)
			{
				gameUpdate(globals); // Update game state
				gameRender(); // Update game screen
			}
			
			// Draw game screen
			paintScreen();
			
			// Let the process sleep for just a little bit.
			// Hmmm game update and game render are on the same time frame.
			// - Sometimes I want updates to occur faster than renders!
			//
			try 
			{
				Thread.sleep((long) (framesPerSecond * 1000));
			} 
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
		
		System.exit(0);
	}
	
	/**
	 * 
	 */
	private final void gameRender()
	{
		/*
		if(doubleBufferedImage == null)
		{
			doubleBufferedImage = (BufferedImage) createImage(this.getWidth(), this.getHeight());
			
			if(doubleBufferedImage == null)
			{
				System.out.println("doubleBufferedImage is null!");
				return;
			}
			else
				doubleBufferedGraphics = (Graphics2D) doubleBufferedImage.getGraphics();
		}
		*/

		// Yeah why am I making the game render fail after 2 checks?
		// Why not... make it try indefinitely?
		while((doubleBufferedImage = (BufferedImage) createImage(this.getWidth(), this.getHeight())) == null);
		
		doubleBufferedGraphics = (Graphics2D) doubleBufferedImage.getGraphics();
		doubleBufferedGraphics.setColor(getBackground());
		doubleBufferedGraphics.clearRect(0, 0, this.getWidth(), this.getHeight());
		doubleBufferedGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		doubleBufferedGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		doubleBufferedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		
		// Do actual drawing here
		drawScreen(doubleBufferedGraphics);
	}
	
	/**
	 * 
	 */
	private final void paintScreen()
	{
		Graphics2D onscreenGFX = (Graphics2D) this.getGraphics();
		
		if(onscreenGFX != null && doubleBufferedImage != null)
			onscreenGFX.drawImage(doubleBufferedImage, 0, 0, null);
		
		Toolkit.getDefaultToolkit().sync();
		
		onscreenGFX.dispose();
	}
	
	/**
	 * 
	 * @return
	 */
	public double getFramesPerSecond()
	{
		return framesPerSecond;
	}

	/**
	 * This method is set to protected status, as it should not be called
	 * externally. However, it is recommended that sub-classes of Runner use
	 * this method in order to get correct mouse position information within
	 * the content area.<br>
	 * 
	 * @param e
	 */
	protected void setMouseXY(MouseEvent e)
	{
		Container topWindowLayer = this.getTopLevelAncestor();
		if(topWindowLayer != null)
		{
			mouseX = e.getX() - topWindowLayer.getInsets().left;
			mouseY = e.getY() - topWindowLayer.getInsets().top;
		}
	}
	
	/**
	 * 
	 * @return
	 */
	public int getMouseX()
	{
		return mouseX;
	}
	
	/**
	 * 
	 * @return
	 */
	public int getMouseY()
	{
		return mouseY;
	}
}
